Workflow dashboard with mermaid.js
工作流概述
这是一个包含12个节点的复杂工作流,主要用于自动化处理各种任务。
工作流源代码
{
"id": "Um37boya1U0mnCjS",
"meta": {
"instanceId": "fb924c73af8f703905bc09c9ee8076f48c17b596ed05b18c0ff86915ef8a7c4a",
"templateCredsSetupCompleted": true
},
"name": "Workflow dashboard with mermaid.js",
"tags": [],
"nodes": [
{
"id": "c1f74b3a-2ae6-4491-ac02-e1e0fd188664",
"name": "When clicking ‘Test workflow’",
"type": "n8n-nodes-base.manualTrigger",
"position": [
1220,
560
],
"parameters": {},
"typeVersion": 1
},
{
"id": "2aef0899-91bb-4141-9ec1-def1c31806ae",
"name": "Respond with Mermaid",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2640,
560
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/plain"
}
]
}
},
"respondWith": "text",
"responseBody": "={{ $json.mermaidChart }}"
},
"typeVersion": 1.1
},
{
"id": "2c60a2e2-9f35-45dc-94d1-daf75314e934",
"name": "List workflows",
"type": "n8n-nodes-base.n8n",
"position": [
1620,
360
],
"parameters": {
"filters": {},
"requestOptions": {}
},
"credentials": {
"n8nApi": {
"id": "eW7IdTFt4ARJbEwR",
"name": "Ted n8n account"
}
},
"typeVersion": 1
},
{
"id": "ce4e49b9-e1ab-44d1-9490-5c685c9023d9",
"name": "Aggregate",
"type": "n8n-nodes-base.aggregate",
"position": [
1980,
360
],
"parameters": {
"options": {},
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "wf_data"
}
]
}
},
"typeVersion": 1
},
{
"id": "bc48416a-01ff-45f4-9bf2-9f4a39054b54",
"name": "Single workflow",
"type": "n8n-nodes-base.n8n",
"position": [
1620,
560
],
"parameters": {
"operation": "get",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.query.wfid }}"
},
"requestOptions": {}
},
"credentials": {
"n8nApi": {
"id": "eW7IdTFt4ARJbEwR",
"name": "Ted n8n account"
}
},
"typeVersion": 1
},
{
"id": "85f28981-544b-4510-b1ee-d4d538455074",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"position": [
1420,
460
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "load page",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "array",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ Object.keys($json?.query)}}",
"rightValue": "wfid"
}
]
},
"renameOutput": true
},
{
"outputKey": "has wfid",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "a4c4c624-2ff5-4fc0-9bdb-802412a5d92f",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ Object.keys($json.query).join(',') }}",
"rightValue": "wfid"
}
]
},
"renameOutput": true
}
]
},
"options": {
"looseTypeValidation": true
}
},
"typeVersion": 3
},
{
"id": "95e0b67b-5e5b-4433-9822-da86900c12ca",
"name": "Send Page",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2640,
360
],
"parameters": {
"options": {},
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>n8n Workflow Visualizer</title>
<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
<script src=\"https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js\"></script>
<style>
.card-img-container {
height: 250px;
overflow: hidden;
}
.card-img-container img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
}
</style>
</head>
<body>
<div class=\"container mt-4\">
<h2>n8n automation flowcharts with mermaid.js</h2>
<div id=\"workflows-container\"></div>
</div>
<hr class=\"featurette-divider border-dark\" />
<section id=\"about\" class=\"container mt-3\">
<h2 class=\"text-center mb-5\">About</h2>
<div class=\"row\">
<div class=\"col-lg-3 text-center\">
<img src=\"https://gravatar.com/avatar/a551e67c6fe7affd5f882a527dee154bb6c3ac90cf878326accb3fb3ec77c8a6?r=pg&d=retro&size=200\" alt=\"Eduard\" class=\"rounded-circle mb-3\" width=\"140\" height=\"140\" />
<h3 class=\"fw-normal\">Eduard</h3>
<p><a class=\"btn btn-warning\" href=\"https://n8n.io/creators/eduard/\" target=\"_blank\">More templates</a></p>
<p><a class=\"btn btn-outline-primary\" href=\"https://www.linkedin.com/in/parsadanyan/\" target=\"_blank\">LinkedIn</a></p>
</div>
<div class=\"col-lg-9 text-center\">
<div class=\"card shadow-sm mb-3\">
<div class=\"card-img-container\">
<img src=\"https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/Untitled_design_6_18de4ce8f4.png\" class=\"card-img-top\" alt=\"How to work with XML and SQL using n8n\" />
</div>
<div class=\"card-body\">
<h5 class=\"card-title\">🦅 Workflow Dashboard for n8n</h5>
<p class=\"card-text\">Get an overview of your n8n instance. This dashboard displays all workflows, nodes, and tags on a single page.</p>
<a href=\"https://n8n.io/workflows/2269-get-a-birds-eye-view-of-your-n8n-instance-with-the-workflow-dashboard/\" class=\"btn btn-primary\" target=\"_blank\">Grab the template!</a>
</div>
</div>
</div>
</div>
</section>
<script>
// JSON object containing workflow data with base webhook URL
const workflowsData = {
baseWorkflowUrl: \"{{ `${$json.instance_url}/workflow/`.replace(/([^:]\/)\/+/g, '$1') }}\",
baseWebhookUrl : \"{{ `${$json.instance_url}/${$json.webhook_path}/${$json.webhook_name}?wfid=`.replace(/([^:]\/)\/+/g, '$1') }}\",
workflows : {{ JSON.stringify($json.wf_data) }}
};
document.addEventListener('DOMContentLoaded', () => {
const workflowsContainer = document.getElementById('workflows-container');
// Render initial page layout
renderWorkflows(workflowsData.workflows);
function renderWorkflows(workflows) {
workflows.forEach(workflow => {
const card = createWorkflowCard(workflow);
workflowsContainer.appendChild(card);
});
}
function createWorkflowCard(workflow) {
const card = document.createElement('div');
card.className = 'card mb-3';
card.innerHTML = `
<div class=\"card-body\">
<h5 class=\"card-title d-flex align-items-center\">
${workflow.name}
<span class=\"badge bg-light-subtle border border-light-subtle text-light-emphasis rounded-pill ms-2\">
<a href=\"${workflowsData.baseWorkflowUrl}${workflow.id}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary-emphasis text-decoration-none\" title=\"Open workflow in a new window\"> 🔗 </a>
</span>
</h5>
<button class=\"btn btn-primary show-workflow-btn\" data-workflow-id=\"${workflow.id}\">Show Workflow</button>
<div class=\"mermaid-container mt-3\" style=\"display: none;\"></div>
</div>
`;
const showWorkflowBtn = card.querySelector('.show-workflow-btn');
const mermaidContainer = card.querySelector('.mermaid-container');
let isLoaded = false;
showWorkflowBtn.addEventListener('click', () => {
if (!isLoaded) {
fetchWorkflowDiagram(workflow.id, mermaidContainer);
isLoaded = true;
showWorkflowBtn.textContent = 'Hide Workflow';
} else {
if (mermaidContainer.style.display === 'none') {
mermaidContainer.style.display = 'block';
showWorkflowBtn.textContent = 'Hide Workflow';
} else {
mermaidContainer.style.display = 'none';
showWorkflowBtn.textContent = 'Show Workflow';
}
}
});
return card;
}
function fetchWorkflowDiagram(workflowId, container) {
const webhookUrl = `${workflowsData.baseWebhookUrl}${workflowId}`;
fetch(webhookUrl)
.then(response => response.text())
.then(mermaidCode => {
container.innerHTML = mermaidCode;
container.style.display = 'block';
mermaid.init(undefined, container);
})
.catch(error => {
console.error('Error fetching workflow diagram:', error);
container.innerHTML = '<p class=\"text-danger\">Error loading workflow diagram.</p>';
container.style.display = 'block';
});
}
// Initialize mermaid
mermaid.initialize({ startOnLoad: false });
});
</script>
<script>
// Blog posts fetching and rendering
document.addEventListener('DOMContentLoaded', () => {
const blogPostsContainer = document.getElementById('blog-posts-container');
const authors = ['Yulia Dmitrievna', 'Eduard Parsadanyan'];
const maxPosts = 3;
fetch('https://blog.n8n.io/rss/')
.then(response => response.text())
.then(str => new window.DOMParser().parseFromString(str, \"text/xml\"))
.then(data => {
const items = data.querySelectorAll(\"item\");
let postCount = 0;
items.forEach(el => {
if (postCount >= maxPosts) return;
const author = el.querySelector(\"dc\\:creator\").textContent.trim();
if (authors.includes(author)) {
const title = el.querySelector(\"title\").textContent;
const link = el.querySelector(\"link\").textContent;
const imageUrl = el.querySelector(\"media\\:content\").getAttribute(\"url\");
const card = document.createElement('div');
card.className = 'col-md-4 mb-4';
card.innerHTML = `
<div class=\"card h-100\">
<img src=\"${imageUrl}\" class=\"card-img-top\" alt=\"${title}\">
<div class=\"card-body\">
<h5 class=\"card-title\">${title}</h5>
<p class=\"card-text\">By ${author}</p>
<a href=\"${link}\" class=\"btn btn-primary\" target=\"_blank\">Read More</a>
</div>
</div>
`;
blogPostsContainer.appendChild(card);
postCount++;
}
});
})
.catch(error => {
console.error('Error fetching blog posts:', error);
blogPostsContainer.innerHTML = '<p class=\"text-danger\">Error loading blog posts.</p>';
});
});
</script>
<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js\"></script>
</body>
</html>"
},
"typeVersion": 1.1
},
{
"id": "7f964438-a211-40bf-a991-a93848607513",
"name": "Prepare workflow list",
"type": "n8n-nodes-base.set",
"position": [
1800,
360
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "1ce915da-7ee4-487c-9233-0b603d4a913b",
"name": "wf_data",
"type": "object",
"value": "={
\"id\" :\"{{ $json.id }}\",
\"name\":\"{{ $json.name }}\"
}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "d379a0b6-aaee-4f4d-91be-74d79c160bb8",
"name": "CONFIG",
"type": "n8n-nodes-base.set",
"position": [
2300,
360
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "07da029f-3de3-45cb-8d33-798fa1a3d529",
"name": "instance_url",
"type": "string",
"value": "={{$env[\"N8N_PROTOCOL\"]}}://{{$env[\"N8N_HOST\"]}}"
},
{
"id": "f7dae7f3-e51b-4da3-ac8b-d198747679d2",
"name": "webhook_name",
"type": "string",
"value": "={{ $('Webhook').params.path}}"
},
{
"id": "185e41a7-8b61-46e3-99ea-0b0a66982080",
"name": "webhook_path",
"type": "string",
"value": "={{$env[\"N8N_ENDPOINT_WEBHOOK\"] || \"webhook\"}}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "bfc42a15-130c-4e81-9f89-c07b3bb56928",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
1800,
560
],
"parameters": {
"jsCode": "const workflow = $input.first().json;
// Extract nodes from the workflow
const nodes = workflow.nodes || [];
// Node types to exclude
const excludedNodeTypes = ['n8n-nodes-base.stickyNote'];
// Define shapes and their corresponding brackets
// https://mermaid.js.org/syntax/flowchart.html
const shapes = {
'rect': ['[', ']'],
'rhombus': ['{', '}'],
'circle': ['((', '))'],
'hexagon': ['{{', '}}'],
'subroutine': ['[[', ']]'],
'parallelogram': ['[\/', '\/]'],
'wait': ['(', ')']
// Add more shapes here as needed
};
// Define special shapes for specific node types
const specialShapes = {
'n8n-nodes-base.if': 'rhombus',
'n8n-nodes-base.switch': 'rhombus',
'n8n-nodes-base.code': 'subroutine',
'n8n-nodes-base.executeWorkflow': 'subroutine',
'n8n-nodes-base.httpRequest':'parallelogram',
'n8n-nodes-base.wait':'wait'
// List more special node types
};
// Function to get the shape for a node type
function getNodeShape(nodeType) {
return specialShapes[nodeType] || 'rect';
}
// Create a map of node names to their \"EL<N>\" identifiers, disabled status, and shape
const nodeMap = {};
let nodeCounter = 1;
nodes.forEach((node) => {
if (!excludedNodeTypes.includes(node.type)) {
const shape = getNodeShape(node.type);
nodeMap[node.name] = {
id: `EL${nodeCounter}`,
disabled: node.disabled || false,
shape: shape,
brackets: shapes[shape] || shapes['rect'] // Default to rect if shape not found
};
nodeCounter++;
}
});
// Function to convert special characters to HTML entities
function convertToHTMLEntities(str) {
return str.replaceAll('\"',\"'\").replace(/[^\w\s-]/g, function(char) {
return '&#' + char.charCodeAt(0) + ';';
});
}
// Function to format node text (with strike-through if disabled)
function formatNodeText(nodeName, isDisabled) {
const escapedName = convertToHTMLEntities(nodeName);
return isDisabled ? `<s>${escapedName}</s>` : escapedName;
}
// Generate connections and isolated nodes
const connections = [];
const isolatedNodes = new Set(Object.keys(nodeMap));
if (workflow.connections) {
Object.entries(workflow.connections).forEach(([sourceName, targetConnections]) => {
Object.entries(targetConnections).forEach(([connectionType, targets]) => {
targets.forEach(targetArray => {
targetArray.forEach(target => {
const sourceNode = nodeMap[sourceName];
const targetNode = nodeMap[target.node];
if (sourceNode && targetNode) {
let connectionLine = ` ${sourceNode.id}${sourceNode.brackets[0]}${formatNodeText(sourceName, sourceNode.disabled)}${sourceNode.brackets[1]}`;
if (connectionType === 'main') {
connectionLine += ` -->`;
} else {
connectionLine += ` -.- |${connectionType}|`;
}
connectionLine += ` ${targetNode.id}${targetNode.brackets[0]}${formatNodeText(target.node, targetNode.disabled)}${targetNode.brackets[1]}`;
connections.push(connectionLine);
isolatedNodes.delete(sourceName);
isolatedNodes.delete(target.node);
}
});
});
});
});
}
// Add isolated nodes to the connections array
isolatedNodes.forEach(nodeName => {
const node = nodeMap[nodeName];
connections.push(` ${node.id}${node.brackets[0]}${formatNodeText(nodeName, node.disabled)}${node.brackets[1]}`);
});
// Generate the Mermaid flowchart string
const mermaidChart = `
---
config:
look: neo
theme: default
---
flowchart LR
${connections.join('\n')}`;
// Output the result
return {
json: {
mermaidChart: mermaidChart
}
};"
},
"typeVersion": 2
},
{
"id": "28375139-c433-4c6c-a5ac-3d725c9b79ef",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2120,
100
],
"parameters": {
"color": 3,
"width": 470.91551628883894,
"height": 419.34820384538847,
"content": "## IMPORTANT NOTE FOR CLOUD USERS
### Since the cloud version doesn't support environmental variables, please update the following fields:
1. **instance_url**. Change the `{{$env[\"N8N_PROTOCOL\"]}}://{{$env[\"N8N_HOST\"]}}` expression to your cloud instance URL
2. **webhook_path**. Change the `{{$env[\"N8N_ENDPOINT_WEBHOOK\"] || \"webhook\"}}` simply to the `webhook`. So that the production webhook is called correclty."
},
"typeVersion": 1
},
{
"id": "63245902-69d7-4d75-8cb3-58198208220a",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
1220,
360
],
"webhookId": "dd9e2c5d-6c48-428e-aa54-bef9e369d3b0",
"parameters": {
"path": "dd9e2c5d-6c48-428e-aa54-bef9e369d3b0",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 2
}
],
"active": true,
"pinData": {},
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1",
"saveManualExecutions": true,
"saveDataSuccessExecution": "all"
},
"versionId": "e73fe710-a873-4827-9a3f-2740b5479d62",
"connections": {
"Code": {
"main": [
[
{
"node": "Respond with Mermaid",
"type": "main",
"index": 0
}
]
]
},
"CONFIG": {
"main": [
[
{
"node": "Send Page",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "List workflows",
"type": "main",
"index": 0
}
],
[
{
"node": "Single workflow",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Aggregate": {
"main": [
[
{
"node": "CONFIG",
"type": "main",
"index": 0
}
]
]
},
"List workflows": {
"main": [
[
{
"node": "Prepare workflow list",
"type": "main",
"index": 0
}
]
]
},
"Single workflow": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Prepare workflow list": {
"main": [
[
{
"node": "Aggregate",
"type": "main",
"index": 0
}
]
]
},
"When clicking ‘Test workflow’": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
}
}
}
功能特点
- 自动检测新邮件
- AI智能内容分析
- 自定义分类规则
- 批量处理能力
- 详细的处理日志
技术分析
节点类型及作用
- Manualtrigger
- Respondtowebhook
- N8N
- Aggregate
- Switch
复杂度评估
配置难度:
维护难度:
扩展性:
实施指南
前置条件
- 有效的Gmail账户
- n8n平台访问权限
- Google API凭证
- AI分类服务订阅
配置步骤
- 在n8n中导入工作流JSON文件
- 配置Gmail节点的认证信息
- 设置AI分类器的API密钥
- 自定义分类规则和标签映射
- 测试工作流执行
- 配置定时触发器(可选)
关键参数
| 参数名称 | 默认值 | 说明 |
|---|---|---|
| maxEmails | 50 | 单次处理的最大邮件数量 |
| confidenceThreshold | 0.8 | 分类置信度阈值 |
| autoLabel | true | 是否自动添加标签 |
最佳实践
优化建议
- 定期更新AI分类模型以提高准确性
- 根据邮件量调整处理批次大小
- 设置合理的分类置信度阈值
- 定期清理过期的分类规则
安全注意事项
- 妥善保管API密钥和认证信息
- 限制工作流的访问权限
- 定期审查处理日志
- 启用双因素认证保护Gmail账户
性能优化
- 使用增量处理减少重复工作
- 缓存频繁访问的数据
- 并行处理多个邮件分类任务
- 监控系统资源使用情况
故障排除
常见问题
邮件未被正确分类
检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。
Gmail认证失败
确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。
调试技巧
- 启用详细日志记录查看每个步骤的执行情况
- 使用测试邮件验证分类逻辑
- 检查网络连接和API服务状态
- 逐步执行工作流定位问题节点
错误处理
工作流包含以下错误处理机制:
- 网络超时自动重试(最多3次)
- API错误记录和告警
- 处理失败邮件的隔离机制
- 异常情况下的回滚操作